Descubra o poder do novo auxiliar de Iterador `scan` do JavaScript. Aprenda como ele revoluciona o processamento de streams, gestão de estado e agregação de dados além do `reduce`.
Iterador `scan` do JavaScript: O Elo Perdido para o Processamento Acumulativo de Streams
No cenário em constante evolução do desenvolvimento web moderno, os dados são reis. Estamos constantemente a lidar com fluxos de informação: eventos de utilizadores, respostas de API em tempo real, grandes conjuntos de dados e muito mais. Processar esses dados de forma eficiente e declarativa é um desafio primordial. Durante anos, os desenvolvedores de JavaScript confiaram no poderoso método Array.prototype.reduce para destilar um array a um único valor. Mas e se você precisar ver a jornada, e não apenas o destino? E se precisar observar cada passo intermediário de uma acumulação?
É aqui que uma nova e poderosa ferramenta entra em cena: o auxiliar de Iterador `scan`. Como parte da proposta de Auxiliares de Iterador do TC39, atualmente no Estágio 3, o `scan` está prestes a revolucionar a forma como lidamos com dados sequenciais e baseados em streams em JavaScript. É a contraparte funcional e elegante do `reduce` que fornece o histórico completo de uma operação.
Este guia abrangente levará você a um mergulho profundo no método `scan`. Exploraremos os problemas que ele resolve, a sua sintaxe, os seus poderosos casos de uso, desde simples totais acumulados a complexas gestões de estado, e como ele se encaixa no ecossistema mais amplo do JavaScript moderno e eficiente em memória.
O Desafio Comum: Os Limites do `reduce`
Para apreciar verdadeiramente o que o `scan` traz para a mesa, vamos primeiro revisitar um cenário comum. Imagine que você tem um fluxo de transações financeiras e precisa calcular o saldo acumulado após cada transação. Os dados podem ser assim:
const transactions = [100, -20, 50, -10, 75]; // Depósitos e retiradas
Se você quisesse apenas o saldo final, `Array.prototype.reduce` seria a ferramenta perfeita:
const finalBalance = transactions.reduce((balance, transaction) => balance + transaction, 0);
console.log(finalBalance); // Saída: 195
Isso é conciso e eficaz. Mas e se você precisasse plotar o saldo da conta ao longo do tempo num gráfico? Você precisaria do saldo após cada transação: [100, 80, 130, 120, 195]. O método `reduce` esconde esses passos intermediários de nós; ele fornece apenas o resultado final.
Então, como resolveríamos isso tradicionalmente? Provavelmente recorreríamos a um loop manual com uma variável de estado externa:
const transactions = [100, -20, 50, -10, 75];
const runningBalances = [];
let currentBalance = 0;
for (const transaction of transactions) {
currentBalance += transaction;
runningBalances.push(currentBalance);
}
console.log(runningBalances); // Saída: [100, 80, 130, 120, 195]
Isso funciona, mas tem várias desvantagens:
- Estilo Imperativo: É menos declarativo. Estamos a gerir manualmente o estado (
currentBalance) e a coleção de resultados (runningBalances). - Com Estado e Prolixo: Requer a gestão de variáveis mutáveis fora do loop, o que pode aumentar a carga cognitiva e o potencial para bugs em cenários mais complexos.
- Não Componível: Não é uma operação limpa e encadeável. Interrompe o fluxo do encadeamento de métodos funcionais (como
map,filter, etc.).
Este é precisamente o problema que o auxiliar de Iterador `scan` foi projetado para resolver com elegância e poder.
Um Novo Paradigma: A Proposta dos Auxiliares de Iterador
Antes de saltarmos diretamente para o `scan`, é importante entender o contexto em que ele vive. A proposta dos Auxiliares de Iterador visa tornar os iteradores cidadãos de primeira classe em JavaScript para o processamento de dados. Os iteradores são um conceito fundamental em JavaScript — eles são o motor por trás dos loops for...of, da sintaxe de propagação (...) e dos geradores.
A proposta adiciona um conjunto de métodos familiares, semelhantes aos de array, diretamente ao Iterator.prototype, incluindo:
map(mapperFn): Transforma cada item no iterador.filter(filterFn): Retorna apenas os itens que passam em um teste.take(limit): Retorna os primeiros N itens.drop(limit): Pula os primeiros N itens.flatMap(mapperFn): Mapeia cada item para um iterador e achata o resultado.reduce(reducer, initialValue): Reduz o iterador a um único valor.- E, claro,
scan(reducer, initialValue).
O principal benefício aqui é a avaliação preguiçosa (lazy evaluation). Ao contrário dos métodos de array, que muitas vezes criam novos arrays intermediários na memória, os auxiliares de iterador processam os itens um de cada vez, sob demanda. Isso os torna incrivelmente eficientes em termos de memória para lidar com fluxos de dados muito grandes ou até mesmo infinitos.
Um Mergulho Profundo no Método `scan`
O método `scan` é conceitualmente semelhante ao `reduce`, mas em vez de retornar um único valor final, ele retorna um novo iterador que produz o resultado da função redutora a cada passo. Permite que você veja o histórico completo da acumulação.
Sintaxe e Parâmetros
A assinatura do método é direta e parecerá familiar para qualquer pessoa que já tenha usado o `reduce`.
iterator.scan(reducer [, initialValue])
reducer(accumulator, element, index): Uma função que é chamada para cada elemento no iterador. Ela recebe:accumulator: O valor retornado pela invocação anterior do redutor, ouinitialValuese fornecido.element: O elemento atual sendo processado do iterador de origem.index: O índice do elemento atual.
accumulatorpara a próxima chamada e é também o valor que o `scan` produz (yields).initialValue(opcional): Um valor inicial para usar como o primeiroaccumulator. Se não for fornecido, o primeiro elemento do iterador é usado como o valor inicial, e a iteração começa a partir do segundo elemento.
Como Funciona: Passo a Passo
Vamos rastrear o nosso exemplo de saldo acumulado para ver o `scan` em ação. Lembre-se, o `scan` opera em iteradores, então primeiro precisamos obter um iterador do nosso array.
const transactions = [100, -20, 50, -10, 75];
const initialBalance = 0;
// 1. Obtenha um iterador do array
const transactionIterator = transactions.values();
// 2. Aplique o método scan
const runningBalanceIterator = transactionIterator.scan(
(balance, transaction) => balance + transaction,
initialBalance
);
// 3. O resultado é um novo iterador. Podemos convertê-lo para um array para ver os resultados.
const runningBalances = [...runningBalanceIterator];
console.log(runningBalances); // Saída: [100, 80, 130, 120, 195]
Eis o que acontece por baixo dos panos:
- O `scan` é chamado com um redutor
(a, b) => a + be uminitialValuede0. - Iteração 1: O redutor é chamado com
accumulator = 0(o valor inicial) eelement = 100. Ele retorna100. O `scan` produz100. - Iteração 2: O redutor é chamado com
accumulator = 100(o resultado anterior) eelement = -20. Ele retorna80. O `scan` produz80. - Iteração 3: O redutor é chamado com
accumulator = 80eelement = 50. Ele retorna130. O `scan` produz130. - Iteração 4: O redutor é chamado com
accumulator = 130eelement = -10. Ele retorna120. O `scan` produz120. - Iteração 5: O redutor é chamado com
accumulator = 120eelement = 75. Ele retorna195. O `scan` produz195.
O resultado é uma forma limpa, declarativa e componível de alcançar exatamente o que precisávamos, sem laços manuais ou gestão de estado externo.
Exemplos Práticos e Casos de Uso Globais
O poder do `scan` estende-se muito além de simples totais acumulados. É um primitivo fundamental para o processamento de streams que pode ser aplicado a uma ampla variedade de domínios relevantes para desenvolvedores em todo o mundo.
Exemplo 1: Gestão de Estado e Event Sourcing
Uma das aplicações mais poderosas do `scan` é na gestão de estado, espelhando padrões encontrados em bibliotecas como o Redux. Imagine que você tem um fluxo de ações do utilizador ou eventos da aplicação. Você pode usar o `scan` para processar esses eventos e produzir o estado da sua aplicação em cada ponto no tempo.
Vamos modelar um contador simples com ações de incrementar, decrementar e resetar.
// Uma função geradora para simular um stream de ações
function* actionStream() {
yield { type: 'INCREMENT' };
yield { type: 'INCREMENT' };
yield { type: 'DECREMENT', payload: 2 };
yield { type: 'UNKNOWN_ACTION' }; // Deve ser ignorada
yield { type: 'RESET' };
yield { type: 'INCREMENT', payload: 5 };
}
// O estado inicial da nossa aplicação
const initialState = { count: 0 };
// A função redutora define como o estado muda em resposta às ações
function stateReducer(state, action) {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + (action.payload || 1) };
case 'DECREMENT':
return { ...state, count: state.count - (action.payload || 1) };
case 'RESET':
return { count: 0 };
default:
return state; // IMPORTANTE: Sempre retorne o estado atual para ações não tratadas
}
}
// Use scan para criar um iterador do histórico de estados da aplicação
const stateHistoryIterator = actionStream().scan(stateReducer, initialState);
// Registre cada mudança de estado conforme ela acontece
for (const state of stateHistoryIterator) {
console.log(state);
}
/*
Saída:
{ count: 1 }
{ count: 2 }
{ count: 0 }
{ count: 0 } // ou seja, o estado não foi alterado pela UNKNOWN_ACTION
{ count: 0 } // após RESET
{ count: 5 }
*/
Isto é incrivelmente poderoso. Definimos declarativamente como o nosso estado evolui e usamos o `scan` para criar um histórico completo e observável desse estado. Este padrão é fundamental para depuração com "viagem no tempo" (time-travel debugging), registo de logs e construção de aplicações previsíveis.
Exemplo 2: Agregação de Dados em Grandes Streams
Imagine que você está a processar um ficheiro de log massivo ou um fluxo de dados de sensores IoT que é grande demais para caber na memória. Os auxiliares de iterador brilham aqui. Vamos usar o `scan` para rastrear o valor máximo visto até agora num fluxo de números.
// Um gerador para simular um stream muito grande de leituras de sensor
function* getSensorReadings() {
yield 22.5;
yield 24.1;
yield 23.8;
yield 28.3; // Novo máximo
yield 27.9;
yield 30.1; // Novo máximo
// ... poderia retornar milhões mais
}
const readingsIterator = getSensorReadings();
// Use scan para rastrear a leitura máxima ao longo do tempo
const maxReadingHistory = readingsIterator.scan((maxSoFar, currentReading) => {
return Math.max(maxSoFar, currentReading);
});
// Não precisamos passar um initialValue aqui. O `scan` usará o primeiro
// elemento (22.5) como o máximo inicial e começará a partir do segundo elemento.
console.log([...maxReadingHistory]);
// Saída: [ 24.1, 24.1, 28.3, 28.3, 30.1 ]
Espere, a saída pode parecer um pouco estranha à primeira vista. Como não fornecemos um valor inicial, o `scan` usou o primeiro item (22.5) como o acumulador inicial e começou a produzir a partir do resultado da primeira redução. Para ver o histórico incluindo o valor inicial, podemos fornecê-lo explicitamente, por exemplo com -Infinity.
const maxReadingHistoryWithInitial = getSensorReadings().scan(
(maxSoFar, currentReading) => Math.max(maxSoFar, currentReading),
-Infinity
);
console.log([...maxReadingHistoryWithInitial]);
// Saída: [ 22.5, 24.1, 24.1, 28.3, 28.3, 30.1 ]
Isto demonstra a eficiência de memória dos iteradores. Podemos processar um fluxo de dados teoricamente infinito e obter o máximo acumulado a cada passo, sem nunca manter mais de um valor na memória por vez.
Exemplo 3: Encadeamento com Outros Auxiliares para Lógica Complexa
O verdadeiro poder da proposta dos Auxiliares de Iterador é desbloqueado quando você começa a encadear métodos. Vamos construir um pipeline mais complexo. Imagine um fluxo de eventos de e-commerce. Queremos calcular a receita total ao longo do tempo, mas apenas de pedidos concluídos com sucesso feitos por clientes VIP.
function* getECommerceEvents() {
yield { type: 'PAGE_VIEW', user: 'guest' };
yield { type: 'ORDER_PLACED', user: 'user123', amount: 50, isVip: false };
yield { type: 'ORDER_COMPLETED', user: 'user456', amount: 120, isVip: true };
yield { type: 'ORDER_FAILED', user: 'user789', amount: 200, isVip: true };
yield { type: 'ORDER_COMPLETED', user: 'user101', amount: 75, isVip: true };
yield { type: 'PAGE_VIEW', user: 'user456' };
yield { type: 'ORDER_COMPLETED', user: 'user123', amount: 30, isVip: false }; // Não é VIP
yield { type: 'ORDER_COMPLETED', user: 'user999', amount: 250, isVip: true };
}
const revenueHistory = getECommerceEvents()
// 1. Filtre pelos eventos corretos
.filter(event => event.type === 'ORDER_COMPLETED' && event.isVip)
// 2. Mapeie para apenas o valor do pedido
.map(event => event.amount)
// 3. Use scan para obter o total acumulado
.scan((total, amount) => total + amount, 0);
console.log([...revenueHistory]);
// Vamos rastrear o fluxo de dados:
// - Após filter: { amount: 120 }, { amount: 75 }, { amount: 250 }
// - Após map: 120, 75, 250
// - Após scan (valores produzidos):
// - 0 + 120 = 120
// - 120 + 75 = 195
// - 195 + 250 = 445
// Saída Final: [ 120, 195, 445 ]
Este exemplo é uma bela demonstração de programação declarativa. O código lê-se como uma descrição da lógica de negócio: filtre por pedidos VIP concluídos, extraia o valor e, em seguida, calcule o total acumulado. Cada passo é uma peça pequena, reutilizável e testável de um pipeline maior e eficiente em memória.
`scan()` vs. `reduce()`: Uma Distinção Clara
É crucial solidificar a diferença entre estes dois métodos poderosos. Embora partilhem uma função redutora, o seu propósito e saída são fundamentalmente diferentes.
reduce()é sobre resumo. Ele processa uma sequência inteira para produzir um único valor final. A jornada fica oculta.scan()é sobre transformação e observação. Ele processa uma sequência e produz uma nova sequência do mesmo comprimento, mostrando o estado acumulado a cada passo. A jornada é o resultado.
Aqui está uma comparação lado a lado:
| Característica | iterator.reduce(reducer, initial) |
iterator.scan(reducer, initial) |
|---|---|---|
| Objetivo Principal | Destilar uma sequência até um único valor de resumo. | Observar o valor acumulado a cada passo de uma sequência. |
| Valor de Retorno | Um único valor (Promise se assíncrono) do resultado final acumulado. | Um novo iterador que produz cada resultado intermediário acumulado. |
| Analogia Comum | Calcular o saldo final de uma conta bancária. | Gerar um extrato bancário mostrando o saldo após cada transação. |
| Caso de Uso | Somar números, encontrar um máximo, concatenar strings. | Totais acumulados, gestão de estado, cálculo de médias móveis, observação de dados históricos. |
Comparação de Código
const numbers = [1, 2, 3, 4].values(); // Obtenha um iterador
// Reduce: O destino
const sum = numbers.reduce((acc, val) => acc + val, 0);
console.log(sum); // Saída: 10
// Você precisa de um novo iterador para a próxima operação
const numbers2 = [1, 2, 3, 4].values();
// Scan: A jornada
const runningSum = numbers2.scan((acc, val) => acc + val, 0);
console.log([...runningSum]); // Saída: [1, 3, 6, 10]
Como Usar os Auxiliares de Iterador Hoje
No momento em que este artigo é escrito, a proposta dos Auxiliares de Iterador está no Estágio 3 do processo TC39. Isto significa que está muito perto de ser finalizada e incluída numa futura versão do padrão ECMAScript. Embora possa ainda não estar disponível nativamente em todos os navegadores ou ambientes Node.js, você não precisa esperar para começar a usá-lo.
Você pode usar esses recursos poderosos hoje através de polyfills. A maneira mais comum é usando a biblioteca core-js, que é um polyfill abrangente para recursos modernos de JavaScript.
Para usá-lo, você normalmente instalaria o core-js:
npm install core-js
E depois importaria o polyfill específico da proposta no ponto de entrada da sua aplicação:
import 'core-js/proposals/iterator-helpers';
// Agora você pode usar .scan() e outros auxiliares!
const result = [1, 2, 3].values()
.map(x => x * 2)
.scan((a, b) => a + b, 0);
console.log([...result]); // [2, 6, 12]
Alternativamente, se você está usando um transpilador como o Babel, pode configurá-lo para incluir os polyfills e transformações necessários para propostas do Estágio 3.
Conclusão: Uma Nova Ferramenta para uma Nova Era de Dados
O auxiliar de Iterador `scan` do JavaScript é mais do que apenas um novo método conveniente; ele representa uma mudança em direção a uma maneira mais funcional, declarativa e eficiente em termos de memória de lidar com fluxos de dados. Ele preenche uma lacuna crítica deixada pelo `reduce`, permitindo que os desenvolvedores não apenas cheguem a um resultado final, mas também observem e atuem sobre todo o histórico de uma acumulação.
Ao adotar o `scan` e a proposta mais ampla dos Auxiliares de Iterador, você pode escrever um código que é:
- Mais Declarativo: O seu código expressará mais claramente o que você está a tentar alcançar, em vez de como está a alcançá-lo com loops manuais.
- Mais Componível: Encadeie operações simples e puras para construir pipelines complexos de processamento de dados que são fáceis de ler e de raciocinar.
- Mais Eficiente em Memória: Aproveite a avaliação preguiçosa para processar conjuntos de dados massivos ou infinitos sem sobrecarregar a memória do seu sistema.
À medida que continuamos a construir aplicações mais reativas e com uso intensivo de dados, ferramentas como o `scan` se tornarão indispensáveis. É um primitivo poderoso que permite que padrões sofisticados como event sourcing e processamento de streams sejam implementados de forma nativa, elegante e eficiente. Comece a explorá-lo hoje e estará bem preparado para o futuro do tratamento de dados em JavaScript.